/*
Copyright 2010 Sieric
http://sieric.ca

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package savior.fs;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import savior.messaging.Manager;

public class FTP implements FileSystem {

    private Manager log = Manager.getInstance();
    private String host, port, username, password, startPath;
    FTPClient client;

    public FTP(String _host, String _port, String _username, String _password, String _startPath) {
        host = _host;
        port = _port;
        username = _username;
        password = _password;
        startPath = _startPath;

        // Add a delimiter if there are none at the end
        if (startPath.length() == 0) {
            startPath = "/";
        }
        if (!startPath.endsWith("/")) {
            startPath += '/';
        }
    }

    @Override
    public boolean createDirectory(String _path) throws FSShouldRetryException {
        connect(true);

        try {
            return client.makeDirectory(startPath + _path);
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public boolean deleteDirectory(String _path) throws FSShouldRetryException {
        connect(true);

        try {
            return client.removeDirectory(startPath + _path);
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public boolean deleteFile(String _path) throws FSShouldRetryException {
        connect(true);

        try {
            return client.deleteFile(startPath + _path);
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public boolean directoryExists(String _path) throws FSShouldRetryException {
        connect(true);

        try {
            return client.changeWorkingDirectory(startPath + _path);
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public InputStream getFile(String _path, long _offset) throws FSShouldRetryException {
        connect(true);

        try {
            if (_offset != 0) {
                client.setRestartOffset(_offset);
            }

            client.setFileType(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
            return new FTPInputStream(client.retrieveFileStream(startPath + _path));
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    public FTPFile[] listAll() throws FSShouldRetryException {
        try {
            FTPTimeoutList time = new FTPTimeoutList();
            time.start();
            time.join(60000);
            FTPFile[] result = time.result;
            if (result == null) {
                client = null;
                throw new FSShouldRetryException();
            }
            return result;
        } catch (InterruptedException ex) {
        }
        client = null;
        throw new FSShouldRetryException();
    }

    @Override
    public List<String> listDirectories(String _path) throws FSShouldRetryException {
        connect(true);

        try {
            List<String> result = new ArrayList<String>();
            client.changeWorkingDirectory(startPath + _path);
            for (FTPFile next : listAll()) {
                if ((next.getName().equals(".")) || (next.getName().equals(".."))) {
                    continue;
                }
                if (!next.isDirectory()) {
                    continue;
                }
                result.add(next.getName() + "/");
            }

            return result;
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public List<FileDescription> listFiles(String _path) throws FSShouldRetryException {
        connect(true);
        try {
            List<FileDescription> result = new ArrayList<FileDescription>();
            client.changeWorkingDirectory(startPath + _path);
            for (FTPFile next : listAll()) {
                if ((next.getName().equals(".")) || (next.getName().equals(".."))) {
                    continue;
                }
                if (!next.isFile()) {
                    continue;
                }
                FileDescription fd = new FileDescription();
                fd.name = next.getName();
                fd.size = next.getSize();
                fd.time = next.getTimestamp().getTime().getTime();
                result.add(fd);
            }

            return result;
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public OutputStream putFile(String _path, boolean _append) throws FSShouldRetryException {
        connect(true);

        try {
            client.setFileType(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
            return new FTPOutputStream(client.storeFileStream(startPath + _path));
        } catch (IOException ex) {
            throw new FSShouldRetryException();
        }
    }

    @Override
    public boolean setFileTime(String _path, long _time) throws FSNotSupportedOperationException {
        throw new FSNotSupportedOperationException();
    }

    @Override
    public boolean testConnection() {
        try {
            return connect(false);
        } catch (FSShouldRetryException ex) {
            return false;
        }
    }

    private boolean connect(boolean retryExpOnFail) throws FSShouldRetryException {
        if ((client != null) && (client.isConnected())) {
            return true;
        }
        try {
            // Connect
            client = new FTPClient();
            client.connect(host, Integer.valueOf(port));
            int reply = client.getReplyCode();
            if (!FTPReply.isPositiveCompletion(reply)) {
                client.disconnect();
                if (retryExpOnFail) {
                    throw new FSShouldRetryException();
                } else {
                    return false;
                }
            }

            // Login
            if (!client.login(username, password)) {
                client.disconnect();
                if (retryExpOnFail) {
                    throw new FSShouldRetryException();
                } else {
                    return false;
                }
            }

            // Go to start path
            if (!client.changeWorkingDirectory(startPath)) {
                client.disconnect();
                if (retryExpOnFail) {
                    throw new FSShouldRetryException();
                } else {
                    return false;
                }
            }

            // Set to list all the files
            client.setListHiddenFiles(true);

            return true;
        } catch (Exception ex) {
        }

        if (retryExpOnFail) {
            throw new FSShouldRetryException();
        } else {
            return false;
        }
    }

    private class FTPInputStream extends InputStream {

        private InputStream in;

        public FTPInputStream(InputStream in) {
            this.in = in;
        }

        @Override
        public int available() throws IOException {
            return in.available();
        }

        @Override
        public void close() throws IOException {
            in.close();
            if (!client.completePendingCommand()) {
                throw new IOException("Read file error");
            }
        }

        @Override
        public synchronized void mark(int readlimit) {
            in.mark(readlimit);
        }

        @Override
        public boolean markSupported() {
            return in.markSupported();
        }

        @Override
        public int read() throws IOException {
            return in.read();
        }

        @Override
        public int read(byte b[]) throws IOException {
            return in.read(b);
        }

        @Override
        public int read(byte b[], int off, int len) throws IOException {
            return in.read(b, off, len);
        }

        @Override
        public synchronized void reset() throws IOException {
            in.reset();
        }

        @Override
        public long skip(long n) throws IOException {
            return in.skip(n);
        }
    }

    private class FTPOutputStream extends OutputStream {

        private OutputStream out;

        public FTPOutputStream(OutputStream out) {
            this.out = out;
        }

        @Override
        public void close() throws IOException {
            out.close();
            if (!client.completePendingCommand()) {
                throw new IOException("New file creation error");
            }
        }

        @Override
        public void flush() throws IOException {
            out.flush();
        }

        @Override
        public void write(byte b[]) throws IOException {
            out.write(b);
        }

        @Override
        public void write(byte b[], int off, int len) throws IOException {
            out.write(b, off, len);
        }

        @Override
        public void write(int b) throws IOException {
            out.write(b);
        }
    }

    private class FTPTimeoutList extends Thread {

        public FTPFile[] result;

        @Override
        public void run() {
            try {
                result = client.listFiles("");
            } catch (IOException ex) {
            }
        }
    }
}
